package com.labofjet.system.aop;

import com.labofjet.common.aop.AspectOrder;
import com.labofjet.common.dto.ContextDto;
import com.labofjet.common.service.RedisService;
import com.labofjet.common.util.JSONUtils;
import com.labofjet.common.util.SpringUtils;
import com.labofjet.system.annotation.RedisCacheEvicat;
import com.labofjet.system.constant.CRedisCacheConstant;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.*;

@Aspect
@Component
@Order(AspectOrder.CACHE_ORDER)
public class RedisCacheAspect implements InitializingBean {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    private ApplicationContext applicationContext;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisService redisServiceImpl;

    @Autowired
    private StringRedisSerializer stringRedisSerializer;

    @Autowired
    private JdkSerializationRedisSerializer jdkSerializationRedisSerializer;

    @Autowired
    private Jackson2JsonRedisSerializer jackson2JsonRedisSerializer;


    private Map<String, DefaultRedisScript> scripts = new HashMap<>(); // 不需要支持并发,因为脚本不会再运行期间修改

    /**
     * 处理 spring redis 缓存 evict 注解中allEntries没有作用的BUG
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @SuppressWarnings("unchecked")
    @Around(value = "@annotation(com.labofjet.system.annotation.RedisCacheEvicat)")
    public Object handleCacheEvict(ProceedingJoinPoint pjp) throws Throwable {
        ContextDto contextDto = null;
        Object[] args = pjp.getArgs();
        if (ArrayUtils.isNotEmpty(args)) {
            for (Object arg : args) {
                if (arg instanceof ContextDto) {
                    contextDto = (ContextDto) arg;
                    break;
                }
            }
        }
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        RedisCacheEvicat annotation = AnnotationUtils.findAnnotation(method, RedisCacheEvicat.class);
        CacheEvict[] key = annotation.evict();
        List<CacheEvict> before = new ArrayList<>();
        List<CacheEvict> after = new ArrayList<>();
        for (CacheEvict cacheEvict : key) {
            boolean suucess = cacheEvict.beforeInvocation() ? before.add(cacheEvict) : after.add(cacheEvict);
        }

        this.evictCache(before, contextDto);
        Object object = pjp.proceed();
        this.evictCache(after, contextDto);

        return object;
    }

    /**
     * 删除缓存,支持删除所有page缓存,和界面上选择的多个ID缓存
     *
     * @param list
     */
    private void evictCache(List<CacheEvict> list, ContextDto contextDto) {
        DefaultRedisScript<List> sc = scripts.get("dels");
        for (CacheEvict cacheEvict : list) {
            String[] values = cacheEvict.value();
            for (String value : values) {
                if (cacheEvict.allEntries()) { // 删除page缓存,即allEntries=true的时候,对应page缓存
                    String wholeKey = value + "*"; // 匹配所有的key
                    List<String> cache = (List<String>) redisTemplate.execute(sc, stringRedisSerializer, stringRedisSerializer, Collections.singletonList(wholeKey));
                    log.info("删除page缓存 {}", cache);
                } else { // 删除多个ID缓存,即allEntries=false,即ID缓存,删除操作的时候支持多个ID一起选择删除
                    if (contextDto == null) {
                        log.warn("缓存中传入的contextDto为 null List<CacheEvict>为 {}", list);
                        continue;
                    }
                    List<Integer> ids = JSONUtils.getRequestData(contextDto, List.class); // 要删除哪些ID对应的缓存
                    List<String> wholeKeys = new ArrayList<>();
                    if (CollectionUtils.isNotEmpty(ids)) {
                        for (Integer id : ids) {
                            wholeKeys.add(value + ":" + id);
                        }
                        Long cache = redisServiceImpl.delete(wholeKeys);
                        log.info("删除id缓存数量 {}, 键{}", cache, wholeKeys);
                        if (cache != wholeKeys.size()) {
                            log.warn("Redis删除ID缓存数量和传入的ID数量不一致, 删除id缓存数量 {}, 键{}", cache, wholeKeys);
                        }
                    }
                }
            }
        }
    }


    /**
     * 加载批量lua脚本
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        loadDelScript();
    }

    /**
     * 加载批量删除脚本
     */
    private void loadDelScript() throws IOException {
        // String s = FileUtils.readFileToString(new ClassPathResource(CRedisCacheConstant.SCRIPT_PATH + "/" + "dels.lua").getFile(), Charset.forName("UTF-8"));
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resourceResolver.getResources("classpath*:" + CRedisCacheConstant.SCRIPT_PATH + "/*.lua");
        for (Resource resource : resources) {
            String s = IOUtils.toString(resource.getInputStream(), "UTF-8");
            DefaultRedisScript<List> sc = new DefaultRedisScript<>(s, List.class);
            scripts.put(resource.getFilename().split("\\.")[0], sc); // key为文件名,比如dels.lua,key就是dels
        }
    }
}
